/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.security;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.TestStepConfig;
import com.eviware.soapui.impl.wsdl.support.AbstractTestCaseRunner;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestCase;
import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestStep;
import com.eviware.soapui.impl.wsdl.teststeps.registry.WsdlTestStepFactory;
import com.eviware.soapui.impl.wsdl.teststeps.registry.WsdlTestStepRegistry;
import com.eviware.soapui.model.security.SecurityScan;
import com.eviware.soapui.model.testsuite.Assertable;
import com.eviware.soapui.model.testsuite.TestAssertion;
import com.eviware.soapui.model.testsuite.TestStep;
import com.eviware.soapui.model.testsuite.TestStepResult;
import com.eviware.soapui.model.testsuite.TestStepResult.TestStepStatus;
import com.eviware.soapui.security.result.SecurityResult.ResultStatus;
import com.eviware.soapui.security.result.SecurityScanResult;
import com.eviware.soapui.security.result.SecurityTestStepResult;
import com.eviware.soapui.security.scan.AbstractSecurityScanWithProperties;
import com.eviware.soapui.security.support.SecurityTestRunListener;
import com.eviware.soapui.support.types.StringToObjectMap;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class SecurityTestRunnerImpl extends AbstractTestCaseRunner<SecurityTest, SecurityTestRunContext> implements
        SecurityTestRunner {

    private SecurityTest securityTest;
    // private boolean stopped;
    private SecurityTestRunListener[] securityTestListeners = new SecurityTestRunListener[0];
    private SecurityTestRunListener[] securityTestStepListeners = new SecurityTestRunListener[0];
    private long timeTaken;
    /**
     * holds index of current securityScan out of summary number of scans on
     * SecurityTest level used in main progress bar on SecurityTest
     */
    private int currentScanOnSecurityTestIndex;

    public SecurityTestRunnerImpl(SecurityTest test, StringToObjectMap properties) {
        super(test, properties);
        this.securityTest = test;
        this.currentScanOnSecurityTestIndex = 0;
    }

    public SecurityTestRunContext createContext(StringToObjectMap properties) {
        return new SecurityTestRunContext(this, properties);
    }

    public SecurityTest getSecurityTest() {
        return getTestRunnable();
    }

    @Override
    public TestStepResult runTestStep(TestStep testStep, boolean discard, boolean process) {
        if (!runBeforeSteps(testStep)) {
            return null;
        }
        TestStepResult stepResult = testStep.run(this, getRunContext());
        getResults().add(stepResult);
        setResultCount(getResultCount() + 1);
        // enforceMaxResults( getTestRunnable().getMaxResults() );

        // discard?
        // if( discard && stepResult.getStatus() == TestStepStatus.OK &&
        // getTestRunnable().getDiscardOkResults()
        // && !stepResult.isDiscarded() )
        // {
        // stepResult.discard();
        // }

        return stepResult;
    }

    /**
     * Clones original TestStep for security modification this does not alter the
     * original test step
     *
     * @param sourceTestStep
     * @return TestStep
     */
    public TestStep cloneForSecurityScan(WsdlTestStep sourceTestStep) {
        WsdlTestStep clonedTestStep = null;
        TestStepConfig testStepConfig = (TestStepConfig) sourceTestStep.getConfig().copy();
        WsdlTestStepFactory factory = WsdlTestStepRegistry.getInstance().getFactory(testStepConfig.getType());
        if (factory != null) {
            clonedTestStep = factory.buildTestStep(securityTest.getTestCase(), testStepConfig, false);
            if (clonedTestStep instanceof Assertable) {
                for (TestAssertion assertion : ((Assertable) clonedTestStep).getAssertionList()) {
                    ((Assertable) clonedTestStep).removeAssertion(assertion);
                }
            }
        }
        return clonedTestStep;
    }

    /**
     * Clones original TestStep for security modification this does not alter the
     * original test step
     *
     * @param sourceTestStep
     * @return TestStep
     */
    public static TestStep cloneTestStepForSecurityScan(WsdlTestStep sourceTestStep) {
        WsdlTestStep clonedTestStep = null;
        TestStepConfig testStepConfig = (TestStepConfig) sourceTestStep.getConfig().copy();
        WsdlTestStepFactory factory = WsdlTestStepRegistry.getInstance().getFactory(testStepConfig.getType());
        if (factory != null) {
            clonedTestStep = factory.buildTestStep(sourceTestStep.getTestCase(), testStepConfig, false);
            if (clonedTestStep instanceof Assertable) {
                for (TestAssertion assertion : ((Assertable) clonedTestStep).getAssertionList()) {
                    ((Assertable) clonedTestStep).removeAssertion(assertion);
                }
            }
        }
        return clonedTestStep;
    }

    protected int runCurrentTestStep(SecurityTestRunContext runContext, int currentStepIndex) {
        // flag for detecting if running has been interrupted either by canceling
        // securityScanRequest
        // or if request result is null(backward compatibility for running
        // TestCase )
        boolean jumpExit = false;
        TestStep currentStep = runContext.getCurrentStep();
        securityTestStepListeners = securityTest.getTestStepRunListeners(currentStep);
        if (!currentStep.isDisabled() && !securityTest.skipTest(currentStep)) {
            TestStepResult stepResult = runTestStep(currentStep, true, true);
            if (stepResult == null) {
                jumpExit = true;
            }
            // if( !isRunning() )
            // return -2;

            SecurityTestStepResult securityStepResult = new SecurityTestStepResult(currentStep, stepResult);
            for (int i = 0; i < securityTestListeners.length; i++) {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[i])) {
                    securityTestListeners[i].afterOriginalStep(this, getRunContext(), securityStepResult);
                }
            }

            for (int i = 0; i < securityTestListeners.length; i++) {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[i])) {
                    securityTestListeners[i].beforeStep(this, getRunContext(), stepResult);
                }
            }
            for (int i = 0; i < securityTestStepListeners.length; i++) {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners())
                        .contains(securityTestStepListeners[i])) {
                    securityTestStepListeners[i].beforeStep(this, getRunContext(), stepResult);
                }
            }
            Map<String, List<SecurityScan>> secScanMap = securityTest.getSecurityScansMap();
            if (secScanMap.containsKey(currentStep.getId())) {
                List<SecurityScan> testStepScansList = secScanMap.get(currentStep.getId());
                for (int i = 0; i < testStepScansList.size(); i++) {
                    SecurityScan securityScan = testStepScansList.get(i);
                    // if security scan is disabled skip it.
                    if (securityScan.isDisabled() || securityScan.isSkipFurtherRunning()) {
                        continue;
                    }
                    //if step is failed and scan not applicable to failed steps just set it to skipped
                    //run scan otherwise
                    if (stepResult.getStatus() == TestStepStatus.FAILED && !securityScan.isApplyForFailedStep()) {
                        SecurityScanResult securityScanResult = new SecurityScanResult(securityScan);
                        if (securityScan.getAssertionCount() > 0) {
                            securityScanResult.setStatus(ResultStatus.OK);
                        } else if (securityScan instanceof AbstractSecurityScanWithProperties) {
                            if (((AbstractSecurityScanWithProperties) securityScan).getParameterHolder().getParameterList()
                                    .size() > 0) {
                                securityScanResult.setStatus(ResultStatus.OK);
                            } else {
                                securityScanResult.setStatus(ResultStatus.SKIPPED);
                            }
                        } else {
                            securityScanResult.setStatus(ResultStatus.SKIPPED);
                        }
                        securityStepResult.addSecurityScanResult(securityScanResult);

                        runAfterListeners(runContext, securityScanResult);
                    } else {
                        runContext.setCurrentScanIndex(i);
                        runContext.setCurrentScanOnSecurityTestIndex(currentScanOnSecurityTestIndex++);
                        SecurityScanResult securityScanResult = runTestStepSecurityScan(runContext, currentStep,
                                securityScan);
                        securityStepResult.addSecurityScanResult(securityScanResult);
                        if (securityScanResult.isCanceled()) {
                            jumpExit = true;
                            break;
                        } else if (securityScanResult.getStatus() == ResultStatus.FAILED) {
                            if (getTestRunnable().getFailOnError()) {
                                // setError( stepResult.getError() );
                                fail("Cancelling due to failed security scan");
                            } else {
                                getRunContext().setProperty(SecurityTestRunner.Status.class.getName(),
                                        SecurityTestRunner.Status.FAILED);
                            }
                        }
                    }
                }
                // in case no security scan is executed
                if (securityStepResult.getStatus() == ResultStatus.INITIALIZED) {
                    securityStepResult.setStatus(ResultStatus.UNKNOWN);
                }
                securityTest.putSecurityTestStepResult(currentStep, securityStepResult);
                timeTaken += securityStepResult.getTimeTaken();
            }
            for (int i = 0; i < securityTestStepListeners.length; i++) {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners())
                        .contains(securityTestStepListeners[i])) {
                    securityTestStepListeners[i].afterStep(this, getRunContext(), securityStepResult);
                }
            }
            for (int i = 0; i < securityTestListeners.length; i++) {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[i])) {
                    securityTestListeners[i].afterStep(this, getRunContext(), securityStepResult);
                }
            }
            if (jumpExit) {
                return -2;
            } else if (getGotoStepIndex() != -1) {
                currentStepIndex = getGotoStepIndex() - 1;
                gotoStep(-1);
            }
        }

        runContext.setCurrentStep(currentStepIndex + 1);
        return currentStepIndex;

    }

    /**
     * @param runContext
     * @param securityScanResult
     */
    private void runAfterListeners(SecurityTestRunContext runContext, SecurityScanResult securityScanResult) {
        for (int j = 0; j < securityTestStepListeners.length; j++) {
            if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestStepListeners[j])) {
                securityTestStepListeners[j].afterSecurityScan(this, runContext, securityScanResult);
            }
        }
        for (int j = 0; j < securityTestListeners.length; j++) {
            if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[j])) {
                securityTestListeners[j].afterSecurityScan(this, runContext, securityScanResult);
            }
        }
    }

    public SecurityScanResult runTestStepSecurityScan(SecurityTestRunContext runContext, TestStep currentStep,
                                                      SecurityScan securityScan) {
        SecurityScanResult result = null;
        for (int j = 0; j < securityTestStepListeners.length; j++) {
            if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestStepListeners[j])) {
                securityTestStepListeners[j].beforeSecurityScan(this, runContext, securityScan);
            }
        }
        for (int j = 0; j < securityTestListeners.length; j++) {
            if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[j])) {
                securityTestListeners[j].beforeSecurityScan(this, runContext, securityScan);
            }
        }
        result = securityScan.run(cloneForSecurityScan((WsdlTestStep) currentStep), runContext, this);
        if (securityScan.isRunOnlyOnce()) {
            securityScan.setSkipFurtherRunning(true);
        }
        if (securityTest.getFailOnError() && result.getStatus() == ResultStatus.FAILED) {
            fail("Cancelling due to failed security scan");
        }
        runAfterListeners(runContext, result);
        return result;
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.eviware.soapui.impl.wsdl.support.AbstractTestCaseRunner#notifyBeforeRun
     * ()
     *
     * The order of listeners notifications here is important, security listeners
     * first, so while monitoring execution TestCase log is disabled before
     * WsdlTestCaseDesktopPanel.InternalTestRunListener.beforeRun is executed
     * otherwise SecurityTest execution will temper with functional log.
     */
    protected void notifyBeforeRun() {
        reset();
        if (securityTestListeners == null || securityTestListeners.length == 0) {
            return;
        }

        for (int i = 0; i < securityTestListeners.length; i++) {
            try {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[i])) {
                    securityTestListeners[i].beforeRun(this, getRunContext());
                }
            } catch (Throwable t) {
                SoapUI.logError(t);
            }
        }
        super.notifyBeforeRun();

    }

    private void reset() {
        securityTest.resetAllScansSkipFurtherRunning();
        securityTest.clearSecurityTestStepResultMap();
        timeTaken = 0;
    }

    /*
     * (non-Javadoc)
     *
     * @see
     * com.eviware.soapui.impl.wsdl.support.AbstractTestCaseRunner#notifyAfterRun
     * ()
     *
     * The same as for notifyBeforeRun, security listeners come last
     */
    protected void notifyAfterRun() {
        super.notifyAfterRun();
        if (securityTestListeners == null || securityTestListeners.length == 0) {
            return;
        }

        for (int i = 0; i < securityTestListeners.length; i++) {
            try {
                if (Arrays.asList(getSecurityTest().getSecurityTestRunListeners()).contains(securityTestListeners[i])) {
                    securityTestListeners[i].afterRun(this, getRunContext());
                }
            } catch (Throwable t) {
                SoapUI.logError(t);
            }
        }
    }

    @Override
    public WsdlTestCase getTestCase() {
        return getTestRunnable().getTestCase();
    }

    @Override
    protected void clear(SecurityTestRunContext runContext) {
        super.clear(runContext);
        securityTestListeners = null;
        securityTestStepListeners = null;
    }

    @Override
    protected void runSetupScripts(SecurityTestRunContext runContext) throws Exception {
        super.runSetupScripts(runContext);
        getTestRunnable().runStartupScript(runContext, this);
    }

    @Override
    protected void runTearDownScripts(SecurityTestRunContext runContext) throws Exception {
        getTestRunnable().runTearDownScript(runContext, this);
        super.runTearDownScripts(runContext);
    }

    @Override
    protected void fillInTestRunnableListeners() {
        super.fillInTestRunnableListeners();
        securityTestListeners = getTestRunnable().getSecurityTestRunListeners();
    }

    @Override
    protected void failTestRunnableOnErrors(SecurityTestRunContext runContext) {
        if (runContext.getProperty(SecurityTestRunner.Status.class.getName()) == SecurityTestRunner.Status.FAILED
                && getTestRunnable().getFailSecurityTestOnScanErrors()) {
            fail("Failing due to failed security scan");
        }
    }

    public long getTimeTaken() {
        return timeTaken;
    }

    public long getFunctionalTimeTaken() {
        return super.getTimeTaken();
    }

}
